using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using ParserGenerator.Extensions;

namespace ParserGenerator.Collections
{
    [Serializable,
     DebuggerDisplay(DebugDisplay),
     DebuggerTypeProxy(typeof(ReadOnlySet<>.DebugView))]
    public class ReadOnlySet<T> : ISet<T>, IReadOnlySet<T>, ICollection
    {
        #region mutable set details
        private readonly ISet<T> _set;
        protected ISet<T> Set => _set;
        public int Count => _set.Count;

        public ReadOnlySet(ISet<T> set)
        {
            if (set == null)
                throw new ArgumentNullException(nameof(set));
            _set = set;
        }
        #endregion

        #region readonly set details
        public bool Contains(T item)
        {
            return _set.Contains(item);
        }

        public void CopyTo(T[] array)
        {
            CheckTargetArray(array, 0, Count);
            CopyTo(array, 0);
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            CheckTargetArray(array, arrayIndex, Count);
            foreach (T item in this)
                array[arrayIndex++] = item;
        }

        private void CopyTo(object[] array, int arrayIndex)
        {
            CheckTargetArray(array, arrayIndex, Count);
            foreach (T item in this)
                array[arrayIndex++] = item;
        }

        private void CopyTo(Array array, int arrayIndex)
        {
            CheckTargetArray(array, arrayIndex, Count);
            var itemArray = array as T[];
            if (itemArray != null)
            {
                CopyTo(itemArray, arrayIndex);
                return;
            }
            var objectArray = array as object[];
            if (objectArray != null)
            {
                try
                {
                    CopyTo(objectArray, arrayIndex);
                    return;
                }
                catch (ArrayTypeMismatchException)
                {
                    throw new ArgumentException(SR.InvalidArrayElementType, nameof(array));
                }
            }
            throw new ArgumentException(SR.InvalidArrayElementType, nameof(array));
        }

        public IEnumerator<T> GetEnumerator()
        {
            return _set.GetEnumerator();
        }

        public T[] ToArray()
        {
            if (Count == 0)
                return ArrayExtensions.GetEmpty<T>();
            T[] array = new T[Count];
            CopyTo(array, 0);
            return array;
        }

        public override string ToString()
        {
            return "{" + string.Join(", ", this) + "}";
        }

        private bool IsReadOnly => true;
        private bool IsSynchronized => false;

        [NonSerialized]
        private object _syncRoot;
        private object SyncRoot
        {
            get
            {
                if (_syncRoot == null)
                {
                    var c = _set as ICollection;
                    if (c != null)
                        _syncRoot = c.SyncRoot;
                    else
                        Interlocked.CompareExchange<object>(ref _syncRoot, new object(), null);
                }
                return _syncRoot;
            }
        }

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
        IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();

        void ICollection.CopyTo(Array array, int arrayIndex) => CopyTo(array, arrayIndex);
        int ICollection.Count => Count;
        bool ICollection.IsSynchronized => IsSynchronized;
        object ICollection.SyncRoot => SyncRoot;
        void ICollection<T>.Add(T item) { throw new InvalidOperationException(SR.CollectionReadOnly); }
        void ICollection<T>.Clear() { throw new InvalidOperationException(SR.CollectionReadOnly); }
        bool ICollection<T>.Contains(T item) => Contains(item);
        void ICollection<T>.CopyTo(T[] array, int arrayIndex) => CopyTo(array, arrayIndex);
        bool ICollection<T>.Remove(T item) { throw new InvalidOperationException(SR.CollectionReadOnly); }
        int ICollection<T>.Count => Count;
        bool ICollection<T>.IsReadOnly => IsReadOnly;
        int IReadOnlyCollection<T>.Count => Count;

        bool ISet<T>.Add(T item) { throw new InvalidOperationException(SR.CollectionReadOnly); }
        void ISet<T>.ExceptWith(IEnumerable<T> other) { throw new InvalidOperationException(SR.CollectionReadOnly); }
        void ISet<T>.IntersectWith(IEnumerable<T> other) { throw new InvalidOperationException(SR.CollectionReadOnly); }
        bool ISet<T>.IsProperSubsetOf(IEnumerable<T> other) => IsProperSubsetOf(other);
        bool ISet<T>.IsProperSupersetOf(IEnumerable<T> other) => IsProperSupersetOf(other);
        bool ISet<T>.IsSubsetOf(IEnumerable<T> other) => IsSubsetOf(other);
        bool ISet<T>.IsSupersetOf(IEnumerable<T> other) => IsSupersetOf(other);
        bool ISet<T>.Overlaps(IEnumerable<T> other) => Overlaps(other);
        bool ISet<T>.SetEquals(IEnumerable<T> other) => SetEquals(other);
        void ISet<T>.SymmetricExceptWith(IEnumerable<T> other) { throw new InvalidOperationException(SR.CollectionReadOnly); }
        void ISet<T>.UnionWith(IEnumerable<T> other) { throw new InvalidOperationException(SR.CollectionReadOnly); }
        bool IReadOnlySet<T>.Contains(T item) => Contains(item);
        bool IReadOnlySet<T>.IsProperSubsetOf(IEnumerable<T> other) => IsProperSubsetOf(other);
        bool IReadOnlySet<T>.IsProperSupersetOf(IEnumerable<T> other) => IsProperSupersetOf(other);
        bool IReadOnlySet<T>.IsSubsetOf(IEnumerable<T> other) => IsSubsetOf(other);
        bool IReadOnlySet<T>.IsSupersetOf(IEnumerable<T> other) => IsSupersetOf(other);
        bool IReadOnlySet<T>.Overlaps(IEnumerable<T> other) => Overlaps(other);
        bool IReadOnlySet<T>.SetEquals(IEnumerable<T> other) => SetEquals(other);
        #endregion

        #region items details
        [NonSerialized]
        private ItemCollection _itemsWrapper;
        public ItemCollection Items
        {
            get
            {
                if (_itemsWrapper == null)
                    _itemsWrapper = new ItemCollection(this);
                return _itemsWrapper;
            }
        }

        [Serializable,
         DebuggerTypeProxy(typeof(ReadOnlySet<>.DebugView)),
         DebuggerDisplay(DebugDisplay)]
        public class ItemCollection : ICollection<T>, IReadOnlyCollection<T>, ICollection
        {
            #region mutable collection details
            private readonly ReadOnlySet<T> _owner;
            public int Count => _owner.Count;

            public ItemCollection(ReadOnlySet<T> owner)
            {
                if (owner == null)
                    throw new ArgumentNullException(nameof(owner));
                _owner = owner;
            }
            #endregion

            #region readonly collection details
            public bool Contains(T item)
            {
                return _owner.Contains(item);
            }

            public void CopyTo(T[] array)
            {
                CheckTargetArray(array, 0, Count);
                CopyTo(array, 0);
            }

            public void CopyTo(T[] array, int arrayIndex)
            {
                CheckTargetArray(array, arrayIndex, Count);
                foreach (T item in this)
                    array[arrayIndex++] = item;
            }

            private void CopyTo(object[] array, int arrayIndex)
            {
                CheckTargetArray(array, arrayIndex, Count);
                foreach (T item in this)
                    array[arrayIndex++] = item;
            }

            private void CopyTo(Array array, int arrayIndex)
            {
                CheckTargetArray(array, arrayIndex, Count);
                var itemArray = array as T[];
                if (itemArray != null)
                {
                    CopyTo(itemArray, arrayIndex);
                    return;
                }
                var objectArray = array as object[];
                if (objectArray != null)
                {
                    try
                    {
                        CopyTo(objectArray, arrayIndex);
                        return;
                    }
                    catch (ArrayTypeMismatchException)
                    {
                        throw new ArgumentException(SR.InvalidArrayElementType, nameof(array));
                    }
                }
                throw new ArgumentException(SR.InvalidArrayElementType, nameof(array));
            }

            public Enumerator GetEnumerator()
            {
                return new Enumerator(_owner);
            }

            public T[] ToArray()
            {
                if (Count == 0)
                    return ArrayExtensions.GetEmpty<T>();
                T[] array = new T[Count];
                CopyTo(array, 0);
                return array;
            }

            public override string ToString()
            {
                return "[" + string.Join(", ", this) + "]";
            }

            private bool IsReadOnly => true;
            private bool IsSynchronized => false;
            private object SyncRoot => _owner.SyncRoot;

            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
            IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();

            void ICollection.CopyTo(Array array, int arrayIndex) => CopyTo(array, arrayIndex);
            int ICollection.Count => Count;
            bool ICollection.IsSynchronized => IsSynchronized;
            object ICollection.SyncRoot => SyncRoot;
            void ICollection<T>.Add(T item) { throw new InvalidOperationException(SR.CollectionReadOnly); }
            void ICollection<T>.Clear() { throw new InvalidOperationException(SR.CollectionReadOnly); }
            bool ICollection<T>.Contains(T item) => Contains(item);
            void ICollection<T>.CopyTo(T[] array, int arrayIndex) => CopyTo(array, arrayIndex);
            bool ICollection<T>.Remove(T item) { throw new InvalidOperationException(SR.CollectionReadOnly); }
            int ICollection<T>.Count => Count;
            bool ICollection<T>.IsReadOnly => IsReadOnly;
            int IReadOnlyCollection<T>.Count => Count;
            #endregion

            #region enumerator details
            [Serializable]
            public struct Enumerator : IEnumerator<T>
            {
                private readonly ReadOnlySet<T> _owner;
                private readonly IEnumerator<T> _enumerator;
                private int _index;

                public Enumerator(ReadOnlySet<T> owner)
                {
                    if (owner == null)
                        throw new ArgumentNullException(nameof(owner));
                    _owner = owner;
                    _enumerator = owner._set.GetEnumerator();
                    _index = 0;
                    _current = default(T);
                }

                private void Dispose()
                {
                }

                public bool MoveNext()
                {
                    if (_index < _owner.Count && _enumerator.MoveNext())
                    {
                        _current = _enumerator.Current;
                        _index++;
                        return true;
                    }
                    _index = _owner.Count + 1;
                    _current = default(T);
                    return false;
                }

                private void Reset()
                {
                    _enumerator.Reset();
                    _index = 0;
                    _current = default(T);
                }

                private T _current;
                public T Current
                {
                    get
                    {
                        CheckEnumeratorStatus(_index, _owner.Count);
                        return _current;
                    }
                }

                void IDisposable.Dispose() => Dispose();
                bool IEnumerator.MoveNext() => MoveNext();
                void IEnumerator.Reset() => Reset();
                object IEnumerator.Current => Current;
                T IEnumerator<T>.Current => Current;
            }
            #endregion
        }
        #endregion

        #region readonly set operations
        public bool IsProperSubsetOf(IEnumerable<T> other)
        {
            if (other == null)
                throw new ArgumentNullException(nameof(other));
            return _set.IsProperSubsetOf(other);
        }

        public bool IsProperSupersetOf(IEnumerable<T> other)
        {
            if (other == null)
                throw new ArgumentNullException(nameof(other));
            return _set.IsProperSupersetOf(other);
        }

        public bool IsSubsetOf(IEnumerable<T> other)
        {
            if (other == null)
                throw new ArgumentNullException(nameof(other));
            return _set.IsSubsetOf(other);
        }

        public bool IsSupersetOf(IEnumerable<T> other)
        {
            if (other == null)
                throw new ArgumentNullException(nameof(other));
            return _set.IsSupersetOf(other);
        }

        public bool Overlaps(IEnumerable<T> other)
        {
            if (other == null)
                throw new ArgumentNullException(nameof(other));
            return _set.Overlaps(other);
        }
        #endregion

        #region set comparison
        public bool SetEquals(IEnumerable<T> other)
        {
            if (other == null)
                throw new ArgumentNullException(nameof(other));
            return _set.SetEquals(other);
        }

        public int GetSetHashCode()
        {
            return _set.GetSetHashCode();
        }
        #endregion

        #region utility methods
        private static void CheckEnumeratorStatus(int index, int size)
        {
            if (index == 0 || index == size + 1)
                throw new InvalidOperationException(SR.EnumOpCantHappen);
        }

        private static void CheckTargetArray(T[] array, int arrayIndex, int size)
        {
            if (array == null)
                throw new ArgumentNullException(nameof(array));
            if (arrayIndex < 0)
                throw new ArgumentOutOfRangeException(nameof(arrayIndex), SR.IndexMustBeNonNegative);
            if (size < 0)
                throw new ArgumentOutOfRangeException(nameof(size), SR.SizeMustBeNonNegative);
            if (arrayIndex > array.Length)
                throw new ArgumentOutOfRangeException(nameof(arrayIndex), SR.IndexOutOfRange);
            if (size > array.Length - arrayIndex)
                throw new ArgumentException(SR.TargetArrayTooSmall, nameof(array));
        }

        private static void CheckTargetArray(Array array, int arrayIndex, int size)
        {
            if (array == null)
                throw new ArgumentNullException(nameof(array));
            if (array.Rank != 1)
                throw new ArgumentException(SR.RankMultiDimNotSupported, nameof(array));
            if (array.GetLowerBound(0) != 0)
                throw new ArgumentException(SR.NonZeroLowerBound, nameof(array));
            if (arrayIndex < 0)
                throw new ArgumentOutOfRangeException(nameof(arrayIndex), SR.IndexMustBeNonNegative);
            if (size < 0)
                throw new ArgumentOutOfRangeException(nameof(size), SR.SizeMustBeNonNegative);
            if (arrayIndex > array.Length)
                throw new ArgumentOutOfRangeException(nameof(arrayIndex), SR.IndexOutOfRange);
            if (size > array.Length - arrayIndex)
                throw new ArgumentException(SR.TargetArrayTooSmall, nameof(array));
        }
        #endregion

        #region debug view
        private const string DebugDisplay = "Count = {Count}";

        [Serializable]
        private sealed class DebugView
        {
            private readonly ReadOnlySet<T> _collection;

            public DebugView(ReadOnlySet<T> collection)
            {
                if (collection == null)
                    throw new ArgumentNullException(nameof(collection));
                _collection = collection;
            }

            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
            public T[] Items
            {
                get
                {
                    T[] array = new T[_collection.Count];
                    _collection.CopyTo(array, 0);
                    return array;
                }
            }
        }
        #endregion
    }

    public static class ReadOnlySet
    {
        public static ReadOnlySet<T> GetEmpty<T>()
        {
            return new HashSet<T>().AsReadOnly();
        }

        public static ReadOnlySet<T> New<T>(IEnumerable<T> collection)
        {
            if (collection == null)
                throw new ArgumentNullException(nameof(collection));
            return new HashSet<T>(collection).AsReadOnly();
        }

        public static ReadOnlySet<T> Create<T>(params T[] items)
        {
            if (items == null)
                throw new ArgumentNullException(nameof(items));
            return New(items);
        }
    }
}
